﻿using Microsoft.ProjectServer.Client;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Linq.Expressions;
using System.Text;

namespace PeaceDataCompany.ProjectManager
{
    public class Wrapper
    {
        private string pwaPath;
        private static string projName = string.Empty;
        private static string taskName = string.Empty;
        private static int timeoutSeconds = 10;  // The maximum wait time for a queue job, in seconds.

        private static ProjectContext projContext;

        private StringBuilder log;

        #region Project & task operations
        /// <summary>
        /// Get the GUID of the specified enterprise project type.
        /// </summary>
        /// <param name="eptName"></param>
        /// <returns></returns>
        public Guid GetEptUid(string eptName)
        {
            Guid eptUid = Guid.Empty;
            try
            {
                // Get the list of EPTs that have the specified name. 
                // If the EPT name exists, the list will contain only one EPT.
                var eptList = projContext.LoadQuery(
                    projContext.EnterpriseProjectTypes.Where(
                        ept => ept.Name == eptName));
                projContext.ExecuteQuery();
                eptUid = eptList.First().Id;
            }
            catch (Exception ex)
            {
                string msg = string.Format("GetEptUid: eptName = \"{0}\"{1}",
                    eptName, ex.GetBaseException());
                throw new ArgumentException(msg);
            }
            return eptUid;
        }

        /// <summary>
        /// Create project
        /// </summary>
        public OperationResult CreateProject(string name, string description, DateTime startDate)
        {
            return CreateProject(name, description, startDate, null);
        }

        /// <summary>
        /// Create project with tasks
        /// </summary> 
        public OperationResult CreateProject(string name, string description, DateTime startDate, List<TaskCreationInformation> tasksList)
        {
            var result = new OperationResult();
            bool projCreated = false;

            try
            {
                if (!String.IsNullOrWhiteSpace(name))
                {
                    #region Create project
                    ProjectCreationInformation newProj = new ProjectCreationInformation();
                    newProj.Id = Guid.NewGuid();
                    newProj.Name = name;
                    newProj.Description = description;
                    newProj.Start = startDate;
                    //// Setting the EPT GUID is optional. If no EPT is specified, Project Server uses 
                    //// the default EPT. 
                    //newProj.EnterpriseProjectTypeId = GetEptUid(basicEpt);
                    var allProjects = projContext.LoadQuery(projContext.Projects);
                    projContext.ExecuteQuery();

                    if (allProjects.Select(x => x.Name).Contains(name))
                    {
                        log.AppendLine(String.Format("Project '{0}' already exists.", name));
                        projCreated = false;
                    }
                    else
                    {
                        PublishedProject newPublishedProj = projContext.Projects.Add(newProj);
                        QueueJob qJob = projContext.Projects.Update();
                        JobState jobState = projContext.WaitForQueue(qJob, timeoutSeconds);

                        newPublishedProj.CheckOut();

                        if (jobState == JobState.Success)
                        {
                            projCreated = true;
                            log.AppendLine("Success.");
                            log.AppendLine("Start adding tasks...");
                            DraftProject projCheckedOut = newPublishedProj.CheckOut();
                            var tasks = projContext.LoadQuery(projCheckedOut.Tasks);
                            projContext.ExecuteQuery();

                            if (tasksList != null && tasksList.Any())
                                foreach (var task in tasksList)
                                    projCheckedOut.Tasks.Add(task);

                            //Add the task to the checked out project.
                            qJob = projCheckedOut.Publish(true);
                            jobState = projContext.WaitForQueue(qJob, timeoutSeconds);
                            // Update the published projects collection.
                            qJob = projContext.Projects.Update();
                            jobState = projContext.WaitForQueue(qJob, timeoutSeconds);
                            if (jobState == JobState.Success)
                                log.AppendLine("Success!");
                            else
                            {
                                log.AppendLine(String.Format("There is a problem in the queue. Timeout is {0} seconds.",
                                    timeoutSeconds));
                                log.AppendLine(String.Format("\tQueue JobState: {0}", jobState.ToString()));
                            }
                            log.AppendLine();
                        }
                        else
                        {
                            log.AppendLine(String.Format("There is a problem in the queue. Timeout is {0} seconds.",
                                timeoutSeconds));
                            log.AppendLine(String.Format("\tQueue JobState: {0}", jobState.ToString()));
                            projCreated = false;                           
                        }
                    }
                    #endregion                    
                }
            }
            catch (Exception ex)
            {
                Console.ForegroundColor = ConsoleColor.Red;
                log.AppendLine(String.Format("Error: {0}", ex.Message));
                Console.ResetColor();
            }
            result.Log = log.ToString();
            result.Success = projCreated;
            //return projCreated;
            return result;
        }

        /// <summary>
        /// Delete project by name
        /// </summary>
        public OperationResult DeleteTheProject(string projectName)
        {
            var result = new OperationResult();
            bool isProjectDeleted = false;
            log.AppendLine(String.Format("Deleting the project: '{0}'", projectName));

            var projCollection = projContext.LoadQuery(
                projContext.Projects
                    .Where(p => p.Name == projectName));

            projContext.ExecuteQuery();

            if (projCollection.Any())
            {
                projCollection.First().DeleteObject();

                // Update the collection of published projects.
                QueueJob qJob = projContext.Projects.Update();
                isProjectDeleted = SubmitQueueJob(qJob);

                //Wait for projects update
                bool isProjectStillInList = false;
                int iterationsCount = 0;
                do
                {
                    var prjs = projContext.LoadQuery(
                    projContext.Projects.Where(x => x.Name == projectName));
                    projContext.ExecuteQuery();
                    isProjectStillInList = prjs.Any();
                    iterationsCount++;
                    System.Threading.Thread.Sleep(5000);
                }
                while (isProjectStillInList || iterationsCount > 10);
            }
            else
                throw new ProjectNotExistException(String.Format("No project named '{0}' exists.", projectName)) { Log = log.ToString() };
            //log.AppendLine(String.Format("No project named '{0}' exists.", projectName));

            if (isProjectDeleted)
                log.AppendLine(String.Format("The '{0}' project was deleted.", projectName));
            else
                throw new PWAException(String.Format("There was a problem deleting the project: '{0}'.", projectName)) { Log = log.ToString() };
            //log.AppendLine(String.Format("There was a problem deleting the project: '{0}'.", projectName));                

            result.Log = log.ToString();
            result.Success = isProjectDeleted;
            return result;
        }

        /// <summary>
        /// List the published projects.
        /// </summary>
        public string ListPublishedProjects()
        {
            var projects = GetPublishedProjects();
            log.AppendLine("Project ID : Project name : Created date");
            foreach (PublishedProject pubProj in projects)
            {
                log.AppendLine(String.Format("\t{0} :\t{1} : {2}", pubProj.Id.ToString(), pubProj.Name,
                    pubProj.CreatedDate.ToString()));
            }
            return log.ToString();
        }

        /// <summary>
        /// Returns collection of published projects.
        /// </summary>
        public ProjectCollection GetPublishedProjects()
        {
            // Get the list of projects on the server.
            projContext.Load(projContext.Projects);
            projContext.ExecuteQuery();
            return projContext.Projects;
        }
        /// <summary>
        /// Returns published project with specified properties by project name
        /// </summary>
        /// <param name="projectName">Name of project</param>
        /// <param name="retrievals">Properties need to be retrieved</param>
        /// <returns></returns>
        public PublishedProject GetProject(string projectName, params Expression<Func<PublishedProject, object>>[] retrievals)
        {
            var projCollection = projContext.LoadQuery(
                projContext.Projects
                    .Where(p => p.Name == projectName));

            projContext.ExecuteQuery();

            if (projCollection.Any())
            {
                var project = projCollection.First();
                var desc = new Func<PublishedProject, object>(x => x.Description);
                projContext.Load(project,
                    retrievals);
                projContext.ExecuteQuery();
                return project;
            }
            else
            {
                throw new ArgumentOutOfRangeException(projectName, "There is no published project with name " + projectName);
            }

        }

        /// <summary>
        /// Returns published project with name, description and owner by project name
        /// </summary>
        /// <param name="projectName">Name of project</param>
        /// <returns></returns>
        public PublishedProject GetProject(string projectName)
        {
            return GetProject(projectName,
                x => x.Name,
                x => x.Description,
                x => x.Owner);
        }

        /// <summary>
        /// Returns published tasks collection of project
        /// </summary>
        /// <param name="projectName">Project Name</param>
        /// <returns></returns>
        public PublishedTaskCollection GetProjectTasks(string projectName)
        {
            var project = GetProject(projectName);
            return GetProjectTasks(project);
        }

        /// <summary>
        /// Returns published tasks collection of project
        /// </summary>
        /// <param name="project">Project</param>
        /// <returns></returns>
        public PublishedTaskCollection GetProjectTasks(PublishedProject project)
        {
            var tasks = project.Tasks;
            projContext.Load(tasks);
            return tasks;
        }

        #region Tasks

        #region Delete task
        /// <summary>
        /// Delete task by name from published project
        /// </summary>
        public bool DeleteTask(string name, PublishedProject project)
        {
            var draftProject = project.CheckOut();
            var res = DeleteTask(name, draftProject);
            projContext.Load(draftProject.Publish(true));
            projContext.ExecuteQuery();
            return res;
        }
        /// <summary>
        /// Delete task by guid from published project
        /// </summary>
        public bool DeleteTask(Guid guid, PublishedProject project)
        {
            var draftProject = project.CheckOut();
            var res = DeleteTask(guid, draftProject);
            projContext.Load(draftProject.Publish(true));
            projContext.ExecuteQuery();
            return res;
        }
        /// <summary>
        /// Delete task by name from draft project
        /// </summary>
        public bool DeleteTask(string name, DraftProject draftProject)
        {
            var tasks = draftProject.Tasks;
            projContext.Load(tasks);
            projContext.ExecuteQuery();
            var taskToRemove = GetDraftTask(name, tasks);
            var res = false;
            if (taskToRemove != null)
            {
                tasks.Remove(taskToRemove);
                res = true;
            }
            return res;
        }
        /// <summary>
        /// Delete task by guid from draft project
        /// </summary>
        public bool DeleteTask(Guid guid, DraftProject draftProject)
        {
            var tasks = draftProject.Tasks;
            projContext.Load(tasks);
            projContext.ExecuteQuery();
            var taskToRemove = GetDraftTask(guid, tasks);
            var res = false;
            if (taskToRemove != null)
            {
                tasks.Remove(taskToRemove);
                res = true;
            }
            return res;
        }
        #endregion

        #region Add task
        #region For PublishedProject
        public DraftTask GetDraftTask(string name, DraftTaskCollection tasks)
        {
            return tasks.FirstOrDefault(x => x.Name == name);
        }
        public DraftTask GetDraftTask(Guid guid, DraftTaskCollection tasks)
        {
            return tasks.FirstOrDefault(x => x.Id == guid);
        }

        /// <summary>
        /// Add task to published project
        /// </summary>
        public Guid AddTask(string name, PublishedProject project)
        {
            var draftProject = project.CheckOut();
            var guid = AddTask(name, draftProject);
            projContext.Load(draftProject.Publish(true));
            projContext.ExecuteQuery();
            return guid;
        }

        /// <summary>
        /// Add subtask to published project's task
        /// </summary>
        /// <param name="name">Task's name</param>
        /// <param name="project">Project</param>
        /// <param name="parentId">Parent task's GUID</param>
        /// <returns></returns>
        public Guid AddTask(string name, PublishedProject project, Guid parentId)
        {
            var draftProject = project.CheckOut();
            var guid = AddTask(name, draftProject, string.Empty, parentId, DateTime.MinValue, DateTime.MinValue, false);
            projContext.Load(draftProject.Publish(true));
            projContext.ExecuteQuery();
            return guid;            
        }

        /// <summary>
        /// Adding a task
        /// </summary>
        /// <param name="name">Name</param>
        /// <param name="project">Task's project</param>
        /// <param name="duration">Duration</param>
        /// <param name="parentId">Parent task's GUID</param>
        /// <param name="startDate">Date of start</param>
        /// <param name="endDate">Date of end</param>
        /// <param name="isManual">Intend to manually</param>
        /// <returns>Task id</returns>
        public Guid AddTask(string name, PublishedProject project, string duration, Guid parentId, DateTime startDate, DateTime endDate, bool isManual)
        {
            var draftProject = project.CheckOut();
            var guid = AddTask(name, draftProject, duration, parentId, startDate, endDate, isManual);
            projContext.Load(draftProject.Publish(true));
            projContext.ExecuteQuery();
            return guid; 
        }
        #endregion
        #region For DraftProject
        /// <summary>
        /// Add task to project
        /// </summary>
        public Guid AddTask(string name, DraftProject project)
        {
            return AddTask(name, project, string.Empty, Guid.Empty, DateTime.MinValue, DateTime.MinValue, false);
        }

        /// <summary>
        /// Add subtask to project's task
        /// </summary>
        /// <param name="name">Task's name</param>
        /// <param name="project">Project</param>
        /// <param name="parentId">Parent task's GUID</param>
        /// <returns></returns>
        public Guid AddTask(string name, DraftProject project, Guid parentId)
        {
            return AddTask(name, project, string.Empty, parentId, DateTime.MinValue, DateTime.MinValue, false);
        }

        /// <summary>
        /// Adding a task
        /// </summary>
        /// <param name="name">Name</param>
        /// <param name="project">Task's project</param>
        /// <param name="duration">Duration</param>
        /// <param name="parentId">Parent task's GUID</param>
        /// <param name="startDate">Date of start</param>
        /// <param name="endDate">Date of end</param>
        /// <param name="isManual">Intend to manually</param>
        /// <returns>Task id</returns>
        public Guid AddTask(string name, DraftProject project, string duration, Guid parentId, DateTime startDate, DateTime endDate, bool isManual)
        {
            TaskCreationInformation task = new TaskCreationInformation();            
            task.Name = name;
            task.IsManual = false;
            task.Id = Guid.NewGuid();
            if (!string.IsNullOrWhiteSpace(duration))
                task.Duration = duration;
            if (startDate != DateTime.MinValue)
                task.Start = startDate;
            if (endDate != DateTime.MinValue)
                task.Finish = endDate;
            task.IsManual = isManual;
            if (parentId != Guid.Empty)
                task.ParentId = parentId;
            project.Tasks.Add(task);
            return task.Id;
        }
        #endregion
        #endregion
        #endregion

        /// <summary>
        /// Call WaitForQueue, for the specified maximum time. If the job state is not successful,
        /// write an error message.
        /// </summary>
        private bool SubmitQueueJob(QueueJob qJob)
        {
            bool jobSuccess = false;
            // Calling Load and ExecuteQuery for the queue job is optional. If qJob is 
            // not initialized when you call WaitForQueue, Project Server initializes it.
            //projContext.Load(qJob);
            //projContext.ExecuteQuery();

            JobState jobState = projContext.WaitForQueue(qJob, timeoutSeconds);

            try
            {
                if (jobState == JobState.Success)
                    jobSuccess = true;
                else
                {
                    Console.ForegroundColor = ConsoleColor.Yellow;
                    log.AppendLine(String.Format("There is a problem in the queue. Timeout is {0} seconds.",
                        timeoutSeconds));
                    log.AppendLine(String.Format("\tQueue JobState: {0}", jobState.ToString()));
                    Console.ResetColor();
                }
            }

            catch (Exception ex)
            {
                Console.ForegroundColor = ConsoleColor.Red;
                log.AppendLine(String.Format("Error: {0}", ex.Message));
                Console.ResetColor();
            }
            return jobSuccess;
        }
        #endregion

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="projectWebAppPath">Path to Project Web App</param>
        public Wrapper(string projectWebAppPath)
        {
            pwaPath = projectWebAppPath;
            projContext = new ProjectContext(pwaPath);
            log = new StringBuilder();
        }
        public class OperationResult
        {
            public bool Success { get; set; }
            public string Log { get; set; }            
        }
        public class CreateOperationResult : OperationResult
        {
            public Guid Id { get; set; }
        }
        public class PWAException : Exception
        {
            public string Log { get; set; }

            public PWAException(string message, Exception inner)
                : base(message, inner)
            {
                Log = String.Empty;
            }
            public PWAException(string message)
                : base(message)
            {
                Log = String.Empty;
            }


        }
        public class ProjectNotExistException : PWAException
        {
           public ProjectNotExistException(string message, Exception inner)
                : base(message, inner)
            {

            }
           public ProjectNotExistException(string message)
                : base(message)
            {

            }
        }
    }
}
